package cn.flant.io.tcp;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;

public class MessageReader {
	private InputStream input;
	private Charset charset;
	private byte[] startFlag;
	private byte[] endFlag;
	private byte[] byteBuffer = new byte[2048];
	private int byteOffset = 0;
	private int remainByteSize = 0;
	private boolean containsSeperator;
	private byte[] dataBuffer = new byte[5120];
	private int dataSize = 0;

	public MessageReader(InputStream input) {
		this(input, null);
	}

	public MessageReader(InputStream input, Charset charset) {
		if (charset == null) {
			charset = Charset.defaultCharset();
		}
		this.input = input;
		this.charset = charset;
	}

	public Charset getCharset() {
		return charset;
	}

	public void setBufferSize(int size) {
		if (size <= 0 || size == byteBuffer.length) {
			return;
		}
		byte[] newByteBuffer = new byte[size];
		if (remainByteSize > 0) {
			System.arraycopy(byteBuffer, byteOffset, newByteBuffer, 0, remainByteSize);
		}
		this.byteBuffer = newByteBuffer;
	}

	public int getBufferSize() {
		return byteBuffer.length;
	}

	public boolean isContainsSeperator() {
		return containsSeperator;
	}

	public void setContainsSeperator(boolean containsSeperator) {
		this.containsSeperator = containsSeperator;
	}

	private byte[] getByteArray(byte[] bytes, int offset, int length) {
		if (offset < 0 || offset > bytes.length) {
			throw new IndexOutOfBoundsException("offset(" + offset + ") must >=0 and <=" + bytes.length);
		}
		if (length < 0 || length > bytes.length - offset) {
			throw new IndexOutOfBoundsException("length(" + length + ") must >=0 and <=" + (bytes.length - offset));
		}
		if (bytes.length == (length + offset)) {
			return bytes;
		}
		byte[] newBytes = new byte[length];
		System.arraycopy(bytes, offset, newBytes, 0, length);
		return newBytes;
	}

	public void setStartSeperator(byte[] start, int offset, int length) {
		if (start == null) {
			startFlag = null;
		} else {
			startFlag = getByteArray(start, offset, length);
		}
	}

	public void setStartSeperator(byte[] start) {
		setStartSeperator(start, 0, start == null ? -1 : start.length);
	}

	public void setStartSeperator(String start) {
		if (start == null) {
			startFlag = null;
		} else {
			setStartSeperator(start.getBytes(charset));
		}
	}

	public void setEndSeperator(byte[] end, int offset, int length) {
		if (end == null) {
			endFlag = null;
		} else {
			endFlag = getByteArray(end, offset, length);
		}
	}

	public void setEndSeperator(byte[] end) {
		setEndSeperator(end, 0, end == null ? -1 : end.length);
	}

	public void setEndSeperator(String end) {
		if (end == null) {
			endFlag = null;
		} else {
			setEndSeperator(end.getBytes(charset));
		}
	}

	private boolean readBytesToBuffer() throws IOException {
		if (input == null) {
			return false;
		}
		int readLength = input.read(byteBuffer, byteOffset + remainByteSize, byteBuffer.length - remainByteSize - byteOffset);
		if (readLength < 0) {
			return false;
		}
		remainByteSize += readLength;
		return true;
	}

	private boolean readBytesToBuffer(int minSize) throws IOException {
		while (remainByteSize < minSize) {
			if (byteBuffer.length <= minSize) {
				setBufferSize(minSize * 2);
			}
			if ((byteOffset > 0 && remainByteSize > 0) || (byteBuffer.length - byteOffset <= minSize)) {
				for (int i = 0; i < remainByteSize; i++) {
					byteBuffer[i] = byteBuffer[byteOffset + i];
				}
				byteOffset = 0;
			}
			if (!readBytesToBuffer()) {
				return false;
			}
		}
		return true;
	}

	private int findFlag(byte[] flag) throws IOException {
		int byteSize = remainByteSize + byteOffset;
		int endIdx = byteSize - flag.length + 1;
		int index = -1;
		for (; byteOffset < endIdx; byteOffset++) {
			if (byteBuffer[byteOffset] == flag[0]) {
				if (flag.length != 1) {
					boolean isFind = true;
					for (int j = 1; j < flag.length; j++) {
						if (byteBuffer[byteOffset + j] != flag[j]) {
							isFind = false;
						}
					}
					if (isFind) {
						index = byteOffset;
						byteOffset += flag.length;
						break;
					}
				} else {
					index = byteOffset;
					byteOffset += flag.length;
					break;
				}
			}
		}
		remainByteSize = byteSize - byteOffset;
		return index;
	}

	private boolean matchStartFlag() throws IOException {
		if (startFlag == null || startFlag.length == 0) {
			return true;
		}
		while (true) {
			if (remainByteSize < startFlag.length) {
				if (!readBytesToBuffer(startFlag.length)) {
					return false;
				}
			}
			int idx = findFlag(startFlag);
			if (idx >= 0) {
				if (isContainsSeperator()) {
					copyToDataBuffer(startFlag, 0, startFlag.length);
				}
				return true;
			}
		}
	}

	private boolean matchEndFlag() throws IOException {
		if (endFlag == null || endFlag.length == 0) {
			if (readBytesToBuffer()) {
				copyToDataBuffer(byteBuffer, byteOffset, remainByteSize);
				byteOffset = 0;
				remainByteSize = 0;
			} else {
				return false;
			}
			while (input.available() > 0) {
				if (readBytesToBuffer()) {
					copyToDataBuffer(byteBuffer, byteOffset, remainByteSize);
					byteOffset = 0;
					remainByteSize = 0;
				} else {
					return false;
				}
			}
			return true;
		}
		while (true) {
			if (remainByteSize < endFlag.length) {
				if (!readBytesToBuffer(endFlag.length)) {
					return false;
				}
			}
			int startIndex = byteOffset;
			int idx = findFlag(endFlag);
			if (idx >= 0) {
				copyToDataBuffer(byteBuffer, startIndex, idx - startIndex);
				if (isContainsSeperator()) {
					copyToDataBuffer(endFlag, 0, endFlag.length);
				}
				return true;
			} else {
				copyToDataBuffer(byteBuffer, startIndex, byteOffset - startIndex);
			}
		}
	}

	private void copyToDataBuffer(byte[] bytes, int offset, int length) {
		if (dataBuffer.length - dataSize < length) {
			byte[] newDataBuffer = new byte[dataBuffer.length * 2];
			System.arraycopy(dataBuffer, 0, newDataBuffer, 0, dataBuffer.length);
			this.dataBuffer = newDataBuffer;
		}
		System.arraycopy(bytes, offset, dataBuffer, dataSize, length);
		dataSize += length;
	}

	private boolean readDataToBuffer() throws IOException {
		dataSize = 0;
		try {
			if (!matchStartFlag()) {
				return false;
			}
			if (!matchEndFlag()) {
				return false;
			}
		} catch (IOException e) {
			byteOffset = 0;
			remainByteSize = 0;
			dataSize = 0;
			throw e;
		}
		return true;
	}

	public byte[] readBytesMessage() throws IOException {
		if (readDataToBuffer()) {
			byte[] datas = new byte[dataSize];
			System.arraycopy(dataBuffer, 0, datas, 0, dataSize);
			return datas;
		} else {
			return null;
		}
	}

	public String readStringMessage() throws IOException {
		if (readDataToBuffer()) {
			return new String(dataBuffer, 0, dataSize, charset);
		} else {
			return null;
		}
	}

	public void close() throws IOException {
		if (input != null) {
			input.close();
			input = null;
		}
	}
}
